//
//  UIImage+Extension.swift
//  8btc
//
//  Created by Steven Xie on 2018/11/15.
//  Copyright © 2018 Hangzhou Shichuo Information Technology Co, Ltd. All rights reserved.
//

/// Extension of UIImage for methods related to creating images from colors

import Foundation
import ImageIO
import UIKit

public enum EBTCCompressType {
    case session
    case timeline
}

public extension UIImage {
    /**
     wechat image compress

     - parameter type: session image boundary is 800, timeline is 1280

     - returns: thumb image
     */
    func ebtcCompress(type: EBTCCompressType = .timeline) -> UIImage {
        let size = ebtcImageSize(type: type)
        if let reImage = resizedImage(size: size) {
            let data = reImage.jpegData(compressionQuality: 0.5)!
            return UIImage(data: data)!
        }
        return self
    }

    /**
     get wechat compress image size

     - parameter type: session  / timeline

     - returns: size
     */
    private func ebtcImageSize(type: EBTCCompressType) -> CGSize {
        var width = size.width
        var height = size.height

        var boundary: CGFloat = 1280

        // width, height <= 1280, Size remains the same
        guard width > boundary || height > boundary else {
            return CGSize(width: width, height: height)
        }

        // aspect ratio
        let s = max(width, height) / min(width, height)
        if s <= 2 {
            // Set the larger value to the boundary, the smaller the value of the compression
            let x = max(width, height) / boundary
            if width > height {
                width = boundary
                height = height / x
            } else {
                height = boundary
                width = width / x
            }
        } else {
            // width, height > 1280
            if min(width, height) >= boundary {
                boundary = type == .session ? 800 : 1280
                // Set the smaller value to the boundary, and the larger value is compressed
                let x = min(width, height) / boundary
                if width < height {
                    width = boundary
                    height = height / x
                } else {
                    height = boundary
                    width = width / x
                }
            }
        }
        return CGSize(width: width, height: height)
    }

    /**
     Zoom the picture to the specified size

     - parameter newSize: image size

     - returns: new image
     */
    func resizedImage(size: CGSize) -> UIImage? {
        guard let cgImage = self.cgImage else { return nil }

        let bitsPerComponent = cgImage.bitsPerComponent
        let bytesPerRow = cgImage.bytesPerRow
        let colorSpace = cgImage.colorSpace
        let bitmapInfo = cgImage.bitmapInfo

        guard let context = CGContext(data: nil,
                                      width: Int(size.width),
                                      height: Int(size.height),
                                      bitsPerComponent: bitsPerComponent,
                                      bytesPerRow: bytesPerRow,
                                      space: colorSpace!,
                                      bitmapInfo: bitmapInfo.rawValue) else {
            return nil
        }

        context.interpolationQuality = .high

        context.draw(cgImage, in: CGRect(origin: .zero, size: size))

        let resizedImage = context.makeImage().flatMap {
            UIImage(cgImage: $0)
        }
        return resizedImage
    }

    func calulateSize() -> UInt {
        let data = jpegData(compressionQuality: 0.5)
        if data == nil {
            if let newdata = pngData() {
                let length = newdata.count
                return UInt(length)
            }
            return 0
        }
        return UInt(data!.count)
    }

    func centerClipImage() -> UIImage {
        var tmp = CGFloat(0.0)
        var finalSize = CGRect.zero
        if size.width > size.height {
            tmp = size.height
            finalSize = CGRect(x: 0, y: (size.height - tmp) / 2.0, width: tmp, height: tmp)
        } else if size.width < size.height {
            tmp = size.width
            finalSize = CGRect(x: (size.width - tmp) / 2.0, y: 0, width: tmp, height: tmp)
        } else {
            return self
        }

        var clipImage = self
        if let cg = cgImage, let ref = cg.cropping(to: finalSize) {
            clipImage = UIImage(cgImage: ref)
        }
        return clipImage
    }

    
    // ImageIO
    func resizeIO(size: CGSize) -> UIImage? {
        guard let data = pngData() ?? jpegData(compressionQuality: 1) else { return nil }

        guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil) else { return nil }

        // kCGImageSourceThumbnailMaxPixelSize为生成缩略图的大小。
        // 当设置为800，如果图片本身大于800*600，则生成后图片大小为800*600，
        // 如果源图片为700*500，则生成图片为800*500
        let options: [CFString: Any] = [
            kCGImageSourceCreateThumbnailFromImageIfAbsent: true,
            kCGImageSourceCreateThumbnailWithTransform: true,
            kCGImageSourceShouldCacheImmediately: true,
            kCGImageSourceThumbnailMaxPixelSize: max(size.width, size.height)
        ]
        
        let resizedImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary).flatMap {
            UIImage(cgImage: $0)
        }
        return resizedImage
    }

    func fixedOrientation() -> UIImage? {
        guard imageOrientation != UIImage.Orientation.up else {
            // This is default orientation, don't need to do anything
            return copy() as? UIImage
        }

        guard let cgImage = self.cgImage else {
            // CGImage is not available
            return nil
        }

        guard let colorSpace = cgImage.colorSpace, let ctx = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: cgImage.bitsPerComponent, bytesPerRow: 0, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else {
            return nil // Not able to create CGContext
        }

        var transform: CGAffineTransform = CGAffineTransform.identity

        switch imageOrientation {
        case .down, .downMirrored:
            transform = transform.translatedBy(x: size.width, y: size.height)
            transform = transform.rotated(by: CGFloat.pi)
        case .left, .leftMirrored:
            transform = transform.translatedBy(x: size.width, y: 0)
            transform = transform.rotated(by: CGFloat.pi / 2.0)
        case .right, .rightMirrored:
            transform = transform.translatedBy(x: 0, y: size.height)
            transform = transform.rotated(by: CGFloat.pi / -2.0)
        case .up, .upMirrored:
            break
        @unknown default:
            break
        }

        // Flip image one more time if needed to, this is to prevent flipped image
        switch imageOrientation {
        case .upMirrored, .downMirrored:
            transform.translatedBy(x: size.width, y: 0)
            transform.scaledBy(x: -1, y: 1)
        case .leftMirrored, .rightMirrored:
            transform.translatedBy(x: size.height, y: 0)
            transform.scaledBy(x: -1, y: 1)
        case .up, .down, .left, .right:
            break
        @unknown default:
            break
        }

        ctx.concatenate(transform)

        switch imageOrientation {
        case .left, .leftMirrored, .right, .rightMirrored:
            ctx.draw(self.cgImage!, in: CGRect(x: 0, y: 0, width: size.height, height: size.width))
        default:
            ctx.draw(self.cgImage!, in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
        }

        guard let newCGImage = ctx.makeImage() else { return nil }
        return UIImage(cgImage: newCGImage, scale: 1, orientation: .up)
    }
}

public extension UIImage {
    /**
     Creates a UIImage from a UIColor object
     - parameter color: The color to create the image from
     - parameter width: The width of the image to be created (optional). Default value is 1.0
     - parameter height: The height of the image to be created (optional). Default value is 1.0
     - returns: A UIImage of that specified color otherwise nil
     */
    class func imageFromColor(_ color: UIColor, width: CGFloat = 1, height: CGFloat = 1) -> UIImage {
        let bounds = CGRect(x: 0, y: 0, width: width, height: height)
        let render = UIGraphicsImageRenderer(size: bounds.size)
        return render.image { (context) in
            color.setFill()
            UIRectFill(bounds)
        }
    }
    
    class func alwaysOriginalImage(_ image: UIImage) -> UIImage {
        return image.withRenderingMode(.alwaysOriginal)
    }
    
    func alwaysOriginalImage() -> UIImage{
        return self.withRenderingMode(.alwaysOriginal)
    }
}

public extension UIImage {
    class func gif(data: Data) -> UIImage? {
        // Create source from data
        guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
            print("SwiftGif: Source for the image does not exist")
            return nil
        }

        return UIImage.animatedImageWithSource(source)
    }

    class func gif(url: String) -> UIImage? {
        // Validate URL
        guard let bundleURL = URL(string: url) else {
            print("SwiftGif: This image named \"\(url)\" does not exist")
            return nil
        }

        // Validate data
        guard let imageData = try? Data(contentsOf: bundleURL) else {
            print("SwiftGif: Cannot turn image named \"\(url)\" into NSData")
            return nil
        }

        return gif(data: imageData)
    }

    class func gif(name: String) -> UIImage? {
        // Check for existance of gif
        guard let bundleURL = Bundle.main
            .url(forResource: name, withExtension: "gif") else {
            print("SwiftGif: This image named \"\(name)\" does not exist")
            return nil
        }

        // Validate data
        guard let imageData = try? Data(contentsOf: bundleURL) else {
            print("SwiftGif: Cannot turn image named \"\(name)\" into NSData")
            return nil
        }

        return gif(data: imageData)
    }

    @available(iOS 9.0, *)
    class func gif(asset: String) -> UIImage? {
        // Create source from assets catalog
        guard let dataAsset = NSDataAsset(name: asset) else {
            print("SwiftGif: Cannot turn image named \"\(asset)\" into NSDataAsset")
            return nil
        }

        return gif(data: dataAsset.data)
    }

    internal class func delayForImageAtIndex(_ index: Int, source: CGImageSource!) -> Double {
        var delay = 0.1

        // Get dictionaries
        let cfProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil)
        let gifPropertiesPointer = UnsafeMutablePointer<UnsafeRawPointer?>.allocate(capacity: 0)
        if CFDictionaryGetValueIfPresent(cfProperties, Unmanaged.passUnretained(kCGImagePropertyGIFDictionary).toOpaque(), gifPropertiesPointer) == false {
            return delay
        }

        let gifProperties: CFDictionary = unsafeBitCast(gifPropertiesPointer.pointee, to: CFDictionary.self)

        // Get delay time
        var delayObject: AnyObject = unsafeBitCast(
            CFDictionaryGetValue(gifProperties,
                                 Unmanaged.passUnretained(kCGImagePropertyGIFUnclampedDelayTime).toOpaque()),
            to: AnyObject.self
        )
        if delayObject.doubleValue == 0 {
            delayObject = unsafeBitCast(CFDictionaryGetValue(gifProperties,
                                                             Unmanaged.passUnretained(kCGImagePropertyGIFDelayTime).toOpaque()), to: AnyObject.self)
        }

        delay = delayObject as? Double ?? 0

        //        if delay < 0.1 {
        //            delay = 0.1 // Make sure they're not too fast
        //        }

        return delay
    }

    internal class func gcdForPair(_ a: Int?, _ b: Int?) -> Int {
        var a = a
        var b = b
        // Check if one of them is nil
        if b == nil || a == nil {
            if b != nil {
                return b!
            } else if a != nil {
                return a!
            } else {
                return 0
            }
        }

        // Swap for modulo
        if a! < b! {
            let c = a
            a = b
            b = c
        }

        // Get greatest common divisor
        var rest: Int
        while true {
            rest = a! % b!

            if rest == 0 {
                return b! // Found it
            } else {
                a = b
                b = rest
            }
        }
    }

    internal class func gcdForArray(_ array: [Int]) -> Int {
        if array.isEmpty {
            return 1
        }

        var gcd = array[0]

        for val in array {
            gcd = UIImage.gcdForPair(val, gcd)
        }

        return gcd
    }

    internal class func animatedImageWithSource(_ source: CGImageSource) -> UIImage? {
        let count = CGImageSourceGetCount(source)
        var images = [CGImage]()
        var delays = [Int]()

        // Fill arrays
        for i in 0 ..< count {
            // Add image
            if let image = CGImageSourceCreateImageAtIndex(source, i, nil) {
                images.append(image)
            }

            // At it's delay in cs
            let delaySeconds = UIImage.delayForImageAtIndex(i, source: source)
            delays.append(Int(delaySeconds * 1000.0)) // Seconds to ms
        }

        // Calculate full duration
        let duration: Int = {
            var sum = 0

            for val in delays {
                sum += val
            }

            return sum
        }()

        // Get frames
        let gcd = gcdForArray(delays)
        var frames = [UIImage]()

        var frame: UIImage
        var frameCount: Int
        for i in 0 ..< count {
            frame = UIImage(cgImage: images[i])
            frameCount = Int(delays[i] / gcd)

            for _ in 0 ..< frameCount {
                frames.append(frame)
            }
        }

        // Heyhey
        let animation = UIImage.animatedImage(with: frames,
                                              duration: Double(duration) / 1000.0)
        return animation
    }
}
